#!/usr/bin/python3

import argparse
import contextlib
import hashlib
import json
import logging
import os
import subprocess
import sys
import urllib.request
from pathlib import Path
from typing import Any, List, Optional, Sequence

logger = logging.getLogger(__name__)

FLATPAK_ID = 'org.cockpit_project.CockpitClient'

# Local filenames
MANIFEST_JSON = f'{FLATPAK_ID}.json'
PACKAGES_JSON = f'{FLATPAK_ID}.packages.json'
RELEASES_XML = f'{FLATPAK_ID}.releases.xml'

# Constants related to extra packages
UPSTREAM_REPOS = [
    'cockpit-project/cockpit-files',
    'cockpit-project/cockpit-machines',
    'cockpit-project/cockpit-ostree',
    'cockpit-project/cockpit-podman',
]
DOWNSTREAM_PACKAGES_URL = f'https://raw.githubusercontent.com/flathub/{FLATPAK_ID}/master/{PACKAGES_JSON}'

FLATPAK_DIR = os.path.dirname(__file__)
TOP_DIR = os.path.dirname(os.path.dirname(FLATPAK_DIR))


def write_json(filename: str, content: Any) -> None:
    """Something like g_file_set_contents() for JSON"""
    tmpfile = f'{filename}.tmp'
    try:
        with open(tmpfile, 'w') as file:
            json.dump(content, file, indent=2)
            file.write('\n')

    except BaseException:
        with contextlib.suppress(OSError):
            os.unlink(tmpfile)
        raise
    else:
        os.rename(tmpfile, filename)


def fetch_json(url: str) -> Any:
    with urllib.request.urlopen(url) as file:
        return json.load(file)


def module_for_upstream(repo: str) -> Any:
    logger.info('Fetching release info for %s', repo)
    release = fetch_json(f'https://api.github.com/repos/{repo}/releases/latest')
    url = release['assets'][0]['browser_download_url']
    logger.info('%s', url)
    with urllib.request.urlopen(url) as file:
        sha256 = hashlib.sha256(file.read()).hexdigest()
    logger.info('  %s', sha256)
    return {
        'name': os.path.basename(repo),
        'buildsystem': 'simple',
        'build-commands': ['make install PREFIX=/app'],
        'sources': [
            {
                'type': 'archive',
                'url': url,
                'sha256': sha256
            }
        ]
    }


def create_manifest(
    source_info: Any,
    *,
    branch: str = 'stable',
    extra_modules: Sequence[Any] = ()
) -> Any:
    return {
        'app-id': FLATPAK_ID,
        'runtime': 'org.gnome.Platform',
        'runtime-version': '48',
        'sdk': 'org.gnome.Sdk',
        'command': 'cockpit-client',
        'rename-icon': 'cockpit-client',
        'finish-args': [
            '--talk-name=org.freedesktop.Flatpak',
            '--socket=wayland',
            '--socket=fallback-x11',
            '--device=dri',
            '--share=ipc'
        ],
        'modules': [
            {
                'name': 'cockpit-client',
                'buildsystem': 'autotools',
                'config-opts': [
                    '--enable-cockpit-client',
                    '--with-systemdunitdir=/invalid',
                    'CPPFLAGS=-Itools/mock-build-env',
                    '--with-admin-group=root',
                    '--disable-doc'
                ],
                'make-args': [
                    'build-for-flatpak'
                ],
                'make-install-args': [
                    f'DOWNSTREAM_RELEASES_XML={RELEASES_XML}'
                ],
                'install-rule': 'install-for-flatpak',
                'sources': [
                    source_info,
                    {
                        'type': 'file',
                        'path': RELEASES_XML,
                    }
                ]
            },
            *extra_modules
        ]
    }


def get_packages(origin: Optional[str]) -> List[Any]:
    # 1. If --packages is explicitly given, always use it
    # 2. Otherwise, try to use the already-existing file
    # 3. ... but if it doesn't exist, act like 'downstream' (effectively the default)
    # 4. In any case, write the local file if it changed (or is new)

    try:
        with open(PACKAGES_JSON, 'r') as file:
            local_packages = json.load(file)
    except FileNotFoundError:
        local_packages = None

    if origin == 'none':
        packages = []

    elif origin == 'upstream':
        packages = [module_for_upstream(repo) for repo in UPSTREAM_REPOS]

    elif origin == 'downstream' or local_packages is None:
        packages = fetch_json(DOWNSTREAM_PACKAGES_URL)

    else:
        packages = local_packages

    # Update local file (only if it) changed
    if packages != local_packages:
        write_json(PACKAGES_JSON, packages)

    return packages


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument('--packages', choices=('none', 'upstream', 'downstream'),
                        help="Get extra packages list from upstream/downstream")
    parser.add_argument('--sha256', help="sha256sum of the source release")
    parser.add_argument('location', help="Location of upstream source release (default: build a new one)", nargs='?')
    args = parser.parse_args()

    packages = get_packages(args.packages)

    branch = 'devel'
    if args.location is None:
        try:
            output = subprocess.check_output([f'{TOP_DIR}/tools/make-dist'],
                                             universal_newlines=True)
        except subprocess.CalledProcessError as exc:
            sys.exit(exc.returncode)
        location = {'path': output.rstrip('\n')}

    elif args.location.startswith('https://'):
        branch = 'stable'
        location = {'url': args.location}

    elif args.location.startswith('/') and os.path.exists(args.location):
        location = {'path': args.location}

    else:
        parser.error('If the location is given it must be an absolute path or https URL.')

    if args.sha256:
        location['sha256'] = args.sha256
    elif 'path' in location:
        with open(location['path'], 'rb') as file:
            location['sha256'] = hashlib.sha256(file.read()).hexdigest()
    else:
        parser.error('--sha256 must be provided for https URLs')

    manifest = create_manifest({'type': 'archive', **location},
                               branch=branch, extra_modules=packages)

    write_json(MANIFEST_JSON, manifest)

    Path(RELEASES_XML).touch()

    print(os.path.abspath(MANIFEST_JSON))


if __name__ == '__main__':
    main()
